﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.NetworkInformation;
using System.Net;

namespace NetTraceLib
{
    [Serializable()]
    public class TraceRouteDiscoveryEventArgs
    {
        public PingReply Reply { get; set; }
        public int Attempt { get; set; }
        public int Hop { get; set; }

        public TraceRouteDiscoveryEventArgs(int hop,PingReply reply, int attempt)
        {
            this.Reply = reply;
            this.Attempt = attempt;
            this.Hop = hop;
        }
    }

    public class RouteTracer
    {
        public int Distance { get; protected set; }
        public string Hostname { get; protected set;}
        public IPAddress Address { get; protected set; }
        public IPAddress[] AllAddresses { get; protected set; }
        private Ping ping;
        public List<RouteNode> Nodes { get; protected set; }
        private byte[] magic;
        private static byte[] alphabet = ASCIIEncoding.ASCII.GetBytes("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
        private int maximumDiscoveryAttempts = 5;
        private int discoveryTimeout = 5000;
        private int maximumStatusHistory = 50;
        private int updateTimeout = 5000;
        private int maximumRouteLength = 20;
        public bool FinalDestinationResponded { get; protected set; }

        public event EventHandler<TraceRouteDiscoveryEventArgs> NodeDiscoveryAttempted;

        public RouteTracer(string hostname, int payloadSize)
        {
            this.Hostname = hostname;
            ping = new Ping();
            Nodes = new List<RouteNode>();
            Distance = 1;
            ConstructMagic(payloadSize);
        }

        /// <summary>
        /// When re-tracing a route we might end up with a shorter route. The hold nodes are historically valid,
        /// but not for the current route. We set their address to null, but keep their history intact.
        /// </summary>
        private void InvalidateObsoleteRouteNodeAddresses()
        {
            for (int i = Distance; i < Nodes.Count;i++ )
            { 
                if(Nodes[i] != null)
                {
                    Nodes[i].Address = null;
                    Nodes[i].Obsolete = true;
                }
            }
        }

        public async Task ResolveAddress()
        {
            AllAddresses = await Dns.GetHostAddressesAsync(Hostname);
            if (AllAddresses.Length == 0)
            {
                throw new RouteDiscoveryException(RouteDiscoveryException.RouteDiscoveryError.NoDestinationAddress);
            }

            Address = AllAddresses[0];
        }

        public async Task DiscoverRoute()
        {
            PingOptions pingOptions = new PingOptions(1,true);
            PingReply reply = null;
            RouteNode currentNode;
            
// determine the destination address. This might change so we need to re-determine it every time we re-trace the route.
            await  ResolveAddress();

// once we have the address we attempt to discover the route hop-by-hop.
           for(int hop = 1; hop <= Distance ; hop ++)
           {
               currentNode = null;  // defensive programming.

               pingOptions.Ttl = hop;

               // probe for the node a couple of times.
               for (int attempt = 1; attempt <= maximumDiscoveryAttempts; attempt++)
               {
                   reply = await ping.SendPingAsync(Address, discoveryTimeout, magic, pingOptions);

                   if (NodeDiscoveryAttempted != null)
                       NodeDiscoveryAttempted(this, new TraceRouteDiscoveryEventArgs(hop,reply, attempt));

                   if (reply.Status == IPStatus.Success || reply.Status == IPStatus.TtlExpired)
                       break;
               }

               // there is a technical problem. We cannot be sure if we are already at the destination or not, so we need to exit.
               if (reply.Status != IPStatus.TtlExpired && reply.Status != IPStatus.Success && reply.Status != IPStatus.TimedOut)
                   throw new RouteDiscoveryException(RouteDiscoveryException.RouteDiscoveryError.ErrorProbingHop, hop, reply.Status);

               // construct the route node for the current hop if it doesn't exist, otherwise get previous node
               if(Nodes.Count < hop)
               {
                   currentNode = new RouteNode(hop, magic, maximumStatusHistory, updateTimeout);
                   Nodes.Add(currentNode);
               }
               else 
               {
                   currentNode = Nodes[hop - 1];
                   currentNode.Obsolete = false; // our route length might have increased!
               }

               // we found the final destination!
               if (reply.Status == IPStatus.Success)
               {
                   if (Distance > hop) // it is closer than previously determined!
                   {
                       Distance = hop;
                       InvalidateObsoleteRouteNodeAddresses();
                   }
                   currentNode.Intermediate = false;
                   currentNode.Address = reply.Address;
                   FinalDestinationResponded = true;
               }
               // we found an intermediate node.
               else
               {
                   if (hop >= Distance) // final destination is further than previously estimated.
                   {
                       if (hop < maximumRouteLength)
                           Distance = hop + 1; // at least 1 more hop
                       else
                           FinalDestinationResponded = false;
                   }

                   currentNode.Intermediate = true;

                   // we only have an address if it is a ttl expiry.
                   if (reply.Status == IPStatus.TtlExpired)
                   {
                       currentNode.Address = reply.Address;
                   }
                   else
                   {
                       currentNode.Address = null;
                   }
               }  // end intermediate node

           } // for each hop
            
        }

        /// <summary>
        /// Constructs the contents of the ping given a required packet payload size.
        /// Note that we have to zero terminate the packet contents. The code assumes
        /// small packet sizes.
        /// </summary>
        /// <param name="payloadSize">The size of the payload, excluding the terminating zero.</param>
        private void ConstructMagic(int payloadSize)
        {
            magic = new byte[payloadSize + 1];
            for (int i = 0; i < payloadSize; i++)
            {
                magic[i] = alphabet[i % alphabet.Length];
            }
            magic[payloadSize] = 0;
        }

    }
}
